﻿unit uLunarCalendar;

// 缺陷没有节气

interface

uses
  Windows, Classes, SysUtils;

type
  ERangeError = class(Exception);

  TLunarDate = record
    Year, Month, Day: Word;
    IsLeapMonth: Boolean;
  end;

function SolarToLunar(ADate: TDate): TLunarDate; overload;
function SolarToLunar(aYear, aMonth, aDay:word): TLunarDate; overload;
function LunarToSolar(ADate: TLunarDate): TDate;

function GetLunarDaysOfMonth(AYear, AMonth: Word; AIsLeapMonth: Boolean): Word;

implementation
uses
  Math, StrUtils, DateUtils;

const
  // 지원하는 년도(1881~2100)
  SupportYearStart  = 1881;
  SupportYearEnd    = 2100;
  SupportYearRange  = SupportYearEnd - SupportYearStart;

  SupportSolarDateStartStr = '1881-1-30';
  SupportSolarDateEndStr   = '2101-1-28';

  SupportSolarDateStartYear   = 1881;
  SupportSolarDateStartMonth  = 1;
  SupportSolarDateStartDay    = 30;
  SupportSolarDateEndYear     = 2101;
  SupportSolarDateEndMonth    = 1;
  SupportSolarDateEndDay      = 28;

  StandardDateDelta     = 0     // 1880-12-31까지 일자수
                          + (SupportYearStart - 1) * 365      // 연도
                          + ((SupportYearStart - 1) div 4)    // 윤달처리(Hint 2)
                          - ((SupportYearStart - 1) div 100)
                          + ((SupportYearStart - 1) div 400);
  // 음력 1881년 1월 1일은 양력 1881년 1월 30일 차이
  StandardBetweenStart  = 29;   // 1881-01-30
  // 음)2100-12-29 > 양)2101-01-28
  StandardBetweenEnd    = 28;   // 2101-01-28

  // 음력> 월별 소월/대월/윤월(소)/윤월(대) = 1/2/3/4
  // LunarMonthTable[0][12]가 0이면 윤달 없음
  LunarMonthTable: array[0..SupportYearRange, 0..12] of char = (
      '1212122322121', '1212121221220', '1121121222120', '2112132122122', '2112112121220' // 1881~1890
    , '2121211212120', '2212321121212', '2122121121210', '2122121212120', '1232122121212'
    , '1212121221220', '1121123221222', '1121121212220', '1212112121220', '2121231212121' // 1891~1900
    , '2221211212120', '1221212121210', '2123221212121', '2121212212120', '1211212232212'
    , '1211212122210', '2121121212220', '1212132112212', '2212112112210', '2212211212120' // 1901
    , '1221412121212', '1212122121210', '2112212122120', '1231212122212', '1211212122210'
    , '2121123122122', '2121121122120', '2212112112120', '2212231212112', '2122121212120' // 1911
    , '1212122121210', '2132122122121', '2112121222120', '1211212322122', '1211211221220'
    , '2121121121220', '2122132112122', '1221212121120', '2121221212110', '2122321221212' // 1921
    , '1121212212210', '2112121221220', '1231211221222', '1211211212220', '1221123121221'
    , '2221121121210', '2221212112120', '1221241212112', '1212212212120', '1121212212210' // 1931
    , '2114121212221', '2112112122210', '2211211412212', '2211211212120', '2212121121210'
    , '2212214112121', '2122122121120', '1212122122120', '1121412122122', '1121121222120' // 1941
    , '2112112122120', '2231211212122', '2121211212120', '2212121321212', '2122121121210'
    , '2122121212120', '1212142121212', '1211221221220', '1121121221220', '2114112121222' // 1951
    , '1212112121220', '2121211232122', '1221211212120', '1221212121210', '2121223212121'
    , '2121212212120', '1211212212210', '2121321212221', '2121121212220', '1212112112210' // 1961
    , '2223211211221', '2212211212120', '1221212321212', '1212122121210', '2112212122120'
    , '1211232122212', '1211212122210', '2121121122210', '2212312112212', '2212112112120' // 1971
    , '2212121232112', '2122121212110', '2212122121210', '2112124122121', '2112121221220'
    , '1211211221220', '2121321122122', '2121121121220', '2122112112322', '1221212112120' // 1981
    , '1221221212110', '2122123221212', '1121212212210', '2112121221220', '1211231212222'
    , '1211211212220', '1221121121220', '1223212112121', '2221212112120', '1221221232112' // 1991
    , '1212212122120', '1121212212210', '2112132212221', '2112112122210', '2211211212210'
    , '2221321121212', '2212121121210', '2212212112120', '1232212121212', '1212122122110' // 2001
    , '2121212322122', '1121121222120', '2112112122120', '2211231212122', '2121211212120'
    , '2122121121210', '2124212112121', '2122121212120', '1212121223212', '1211212221220' // 2011
    , '1121121221220', '1212132121222', '1212112121220', '2121211212120', '2122321121212'
    , '1221212121210', '2121221212120', '1232121221212', '1211212212210', '2121123212221' // 2021
    , '2121121212220', '1212112112220', '1221231211221', '2212211211220', '1212212121210'
    , '2123212212121', '2112122122120', '1211212322212', '1211212122210', '2121121122120' // 2031
    , '2212114112122', '2212112112120', '2212121211210', '2212232121211', '2122122121210'
    , '2112122122120', '1231212122212', '1211211221220', '2121121321222', '2121121121220' // 2041
    , '2122112112120', '2122141211212', '1221221212110', '2121221221210', '2114121221221'
    , '2112121212220', '1211211232222', '1211211212220', '1221121121220', '1221214112121' // 2051
    , '2221212112120', '1221212211210', '2121421212211', '2121212212210', '2112112212220'
    , '1213211212221', '2211211212210', '2212121321212', '2212121121210', '2212212112120' // 2061
    , '1212232121212', '1212122121210', '2121212122120', '2112312122212', '2112112122120'
    , '2211211232122', '2121211212120', '2122121121210', '2122123212121', '2121221212120' // 2071
    , '1212121221210', '2121321222121', '2121121221220', '1212112121220', '2123211212122'
    , '2121211211220', '1222121321212', '1221212121210', '2121221212120', '1211241221212' // 2081
    , '1211212212210', '2121121212220', '1212312112222', '1212112112210', '2221211231221'
    , '2212121211220', '1212212121210', '2121214212121', '2112122122120', '1211212142210' // 2091~2100
    , '2121321212212', '2121121122120', '2212112112210', '2223211211212', '2212121211210'
	);

  // 음력> 연도의 일수
  LunarYearDays: array[0..SupportYearRange] of Integer = (
       384, 355, 354, 384, 354, 354, 384, 354, 355, 384 // 1881~1890
     , 355, 384, 354, 354, 383, 355, 354, 384, 355, 384
     , 354, 355, 383, 354, 355, 384, 354, 355, 384, 354 // 1901~1910
     , 384, 354, 354, 384, 355, 354, 384, 355, 384, 354
     , 354, 384, 354, 354, 385, 354, 355, 384, 354, 383
     , 354, 355, 384, 355, 354, 384, 354, 384, 354, 354
     , 384, 355, 355, 384, 354, 354, 384, 354, 384, 354
     , 355, 384, 355, 354, 384, 354, 384, 354, 354, 384 // 1951
     , 355, 354, 384, 355, 353, 384, 355, 384, 354, 355
     , 384, 354, 354, 384, 354, 384, 354, 355, 384, 355
     , 354, 384, 354, 384, 354, 354, 385, 354, 355, 384
     , 354, 354, 383, 355, 384, 355, 354, 384, 354, 354
     , 384, 354, 355, 384, 354, 385, 354, 354, 384, 354 // 2001
     , 354, 384, 355, 384, 355, 354, 384, 354, 354, 384
     , 354, 355, 384, 354, 384, 355, 354, 383, 355, 354
     , 384, 355, 384, 354, 354, 384, 354, 354, 384, 355
     , 355, 384, 354, 384, 354, 354, 384, 354, 355, 384
     , 355, 384, 354, 354, 383, 355, 354, 384, 355, 355 // 2051
     , 383, 354, 384, 354, 355, 384, 354, 355, 384, 354
     , 384, 354, 354, 384, 355, 354, 384, 355, 354, 384
     , 354, 384, 354, 355, 384, 354, 355, 384, 353, 384
     , 355, 354, 384, 355, 354, 384, 354, 354, 384, 354 // 2091~2100
  );

type
  TSolarDate = record
    Year, Month, Day: Word;
  end;

function toSolarDate(AYear, AMonth, ADay: Word): TSolarDate; overload;
begin
  Result.Year   := AYear;
  Result.Month  := AMonth;
  Result.Day    := ADay;
end;

function toLunarDate(AYear, AMonth, ADay: Word; AIsLeapMonth: Boolean): TLunarDate; overload;
begin
  Result.Year         := AYear;
  Result.Month        := AMonth;
  Result.Day          := ADay;
  Result.IsLeapMonth  := AIsLeapMonth;
end;


procedure RangeError(const Msg: string);
  function ReturnAddr: Pointer;
  asm
    MOV     EAX,[EBP+4]
  end;
begin
  raise ERangeError.Create(Msg) at ReturnAddr;
end;


function GetMonthToMonthIndex(AMonth: Word; AIsLeapMonth: Boolean; AMonthTable: string): Integer;
var
  I: Integer;
begin
  Result := AMonth;
  for I := 1 to AMonth do
    if CharInSet(AMonthTable[I], ['3', '4']) then
      Inc(Result);

  if AIsLeapMonth then
    Inc(Result);
end;

function InvalidMonthIndex(AYear: Word;
  AIndexOfMonth: Integer): Boolean;
var
  MonthTable: string;
begin
  Result := False;
  MonthTable := LunarMonthTable[AYear - SupportYearStart];

  if Length(MonthTable) < AIndexOfMonth then
    Exit;

  Result := CharInSet(MonthTable[AIndexOfMonth], ['1'..'4']);
end;

procedure ValidateSolarDate(ADate: TSolarDate);
var
  ErrMsg: string;
begin
  ErrMsg := Format('supported date range: %0:s~%1:s)', [SupportSolarDateStartStr, SupportSolarDateEndStr]);

  // ### 연도 범위 오류
  if ADate.Year < SupportYearStart then
    RangeError(ErrMsg);

  // 양력 지원 시작일 이전
  if (ADate.Year = SupportYearStart) and (ADate.Month = 1) and (ADate.Day <= StandardBetweenStart) then
    RangeError(ErrMsg); // INVALID_RANGE_START

  // 양력 지원 종료일 이후
  if (ADate.Year > SupportYearEnd) and ((ADate.Month > 1) or (ADate.Day > StandardBetweenEnd)) then
    RangeError(ErrMsg); // INVALID_RANGE_END

  // ### 월 범위 오류
  if (ADate.Month < 1) or (ADate.Month > 12) then
    RangeError(Format('''%0:d'' is invalid month.', [ADate.Month]));

  // ### 일 범위 오류
  if (ADate.Day < 1) or (MonthDays[IsLeapYear(ADate.Year)][ADate.Month] < ADate.Day) then
    RangeError(Format('''%0:d'' is invalid day.', [ADate.Day]));
end;

procedure ValidateLunarDate(ADate: TLunarDate);
var
  ErrMsg: string;
  MonthTable: string;
  MonthIndex: Integer;
  DaysOfMonth: Integer;
begin
  // ### 연도 범위 오류
  if (ADate.Year < SupportYearStart) or (ADate.Year > SupportYearEnd) then
  begin
    ErrMsg := Format('supported date range: %0:d-01-01~%1:d-12-31)', [SupportYearStart, SupportYearEnd]);
    RangeError(ErrMsg);
  end;

  // ### 월 범위 오류
  if (ADate.Month < 1) or (ADate.Month > 12) then
    RangeError(Format('''%0:d''is invalid month.', [ADate.Month]));

  // ### 일 범위 오류
  if (ADate.Day < 1) then
    RangeError(Format('''%0:d''is invalid day.', [ADate.Day]));

  MonthTable := LunarMonthTable[ADate.Year - SupportYearStart];
  // 윤달 검증
  if ADate.IsLeapMonth and (not CharInSet(MonthTable[ADate.Month+1], ['3', '4'])) then
    RangeError(Format(' ''%0:d/%1:d'' invalid year or month.', [ADate.Year, ADate.Month]));

  // 대월 소월 검증
  MonthIndex := GetMonthToMonthIndex(ADate.Month, ADate.IsLeapMonth, MonthTable);
  DaysOfMonth := IfThen(CharInSet(MonthTable[MonthIndex], ['1', '3']), 29, 30);
  if ADate.Day > DaysOfMonth then
    RangeError(Format('错误日期：''%0:d年%1:d月%2:d日%4:s''，该月最多%3:d天', [ADate.Year, ADate.Month, ADate.Day, DaysOfMonth, IfThen(ADate.IsLeapMonth, '(闰)', '')]));
end;


{===============================================================================
  # 음력일자를 양력일자로 변환하여 반환
  # Parameter
    ADate: TLunarDate : 변환 대상 음력일자(윤달여부 포함)
  # Return
    TSolarDate: 변환된 양력 일자
===============================================================================}
function LunarToSolar(ADate: TLunarDate): TDate;
  function GetDayCountFromYear(AYear: Word): Integer;
  var
    I: Integer;
  begin
    Result := 0;
    for I := 0 to AYear - SupportYearStart do
      Result := Result + LunarYearDays[I];
  end;

  // 지난달 까지의 일수
  function GetDayCountFromLastMonth(AYear, AMonth: Word; AIsLeap: Boolean): Integer;
  var
    I, MonthIndex: Integer;
    MonthTable: string;
  begin
    Result := 0;
    MonthTable := LunarMonthTable[AYear - SupportYearStart];
    MonthIndex := GetMonthToMonthIndex(AMonth, AIsLeap, MonthTable) - 1;  // 지난달
    for I := 1 to MonthIndex do
      Result := Result + IfThen(CharInSet(MonthTable[I], ['1', '3']), 29, 30);
  end;

var
  I: Integer;
  DayCount: Integer;
  DaysOfYear, DaysOfMonth: Integer;
  mYear,mMonth,mDay:word;
begin
  try
    ValidateLunarDate(ADate);
  except
    raise;
  end;

  ZeroMemory(@Result, SizeOf(Result));

  // ###### 양력일수 구하기 ######
  // [STEP 1] 음력> 기준년 부터 전년도까지의 연도별 일수 증가
  // [STEP 2] 음력> 전월까지 월별 일수 증가(윤달처리 포함)
  // [STEP 3] 음력> 일자 증가

  DayCount := 0;
  // STEP 1
  DayCount := DayCount + GetDayCountFromYear(ADate.Year - 1);
  // STEP 2
  DayCount := DayCount + GetDayCountFromLastMonth(ADate.Year, ADate.Month, ADate.IsLeapMonth);
  // STEP 3
  DayCount := DayCount + ADate.Day;


  // [STEP 4] 기준일의 양음력차 보정
    // 음)1/1 = 양)1/30 경우 아래 로직 상 양력 1/1일부터 계산하므로 29일차 증가
  DayCount := DayCount + StandardBetweenStart;

  // ###### 양력일수에서 음력일자  구하기 ######
  // [STEP 5] 양력> 연도별 일자수 감소하며 연도 추출
  // [STEP 6] 양력> 월별 일자수 감소하면 월 추출
  // [STEP 7] 양력> 잔여일 수 일로 취득

  // STEP 5
  mYear := SupportYearStart - 1;
  for I := 0 to SupportYearRange - 1 do
  begin
    Inc(mYear);

    DaysOfYear := DaysPerYear[IsLeapYear(SupportYearStart + I)];
    if DayCount <= DaysOfYear then
      Break;

    DayCount := DayCount - DaysOfYear;
  end;

  // STEP 6
  mMonth := 0;
  for I := 1 to 12 do
  begin
    Inc(mMonth);

    DaysOfMonth := MonthDays[IsLeapYear(mYear)][I];
    if DayCount <= DaysOfMonth then
      Break;

    DayCount := DayCount - DaysOfMonth;
  end;

  // STEP 7
  mDay := DayCount;

  result := EncodeDate(mYear, mMonth, mDay)
end;

{===============================================================================
  # 양력일자를 음력일자로 변환하여 반환
  # Parameter
    ADate: TSolarDate - 변환 대상 양력일자
  # Return
    TLunarDate - 변환된 음력일자(윤달여부 포함)
===============================================================================}
function SolarToLunar(aYear, aMonth, aDay:word): TLunarDate;
  // 기준년도 부터 요청 년도까지의 일수 합 반환
  function GetDayCountFromYear(AYear: Word): Integer;
  begin
    Result := (AYear * 365) + (AYear div 4) - (AYear div 100) + (AYear div 400);
    Result := Result - StandardDateDelta;
  end;

  // 요청년도의 월까지 일수 합 반환
  function GetDayCountFromMonth(AYear, AMonth: Word): Integer;
  var
    I: Integer;
  begin
    Result := 0;

    for I := 1 to AMonth do
      Inc(Result, MonthDays[IsLeapYear(AYear)][I]);
  end;
var
  I: Integer;
  DayCount, MonDays: Integer;
  MonthTable: string;
begin
  try
    ValidateSolarDate(toSolarDate(aYear, aMonth, aDay));
  except
    raise;
  end;

  ZeroMemory(@Result, SizeOf(Result));

  // ###### 양력일수 구하기 ######
  // [STEP 1] 양력> 기준년 부터 전년도까지 연도별 일자수 증가
  // [STEP 2] 양력> 전월까지 월별 일수 증가(윤년처리 포함)
  // [STEP 3] 양력> 일자 증가

  DayCount := 0;
  // STEP 1
  DayCount := DayCount + GetDayCountFromYear(aYear - 1);
  // STEP 2
  DayCount := DayCount + GetDayCountFromMonth(aYear, AMonth - 1);
  // STEP 3
  DayCount := DayCount + ADay;

  // [STEP 4] 기준일의 양음력차 보정
    // 음)1/1 = 양)1/30 경우 아래 로직 상 양]1/30(음]1/1)일부터 계산해야 하므로 29 빼기
  DayCount := DayCount - StandardBetweenStart;

  // ###### 양력일수에서 음력일자  구하기 ######
  // [STEP 5] 음력> 연도 별 일자 수를 빼며 연도 추출
  // [STEP 6] 음력> 월별 일자 수를 빼며 월 추출
    // 6-1> 평달만 음력월 증가(윤달은 전달의 달번호 계속사용, ex>...3월,윤3월,4월...)
    // 6-2> 음력월의 대/소월 구분(29 or 30)
    // 6-3> 잔여일수(DayCount)가 음력월의 일수보다 작아질때까지 반복
  // [STEP 7] 잔여 일수를 음력일로 처리

  // STEP 5
  Result.Year := SupportYearStart - 1;
  for I := 0 to Length(LunarYearDays) - 1 do
  begin
    Inc(Result.Year);

    if LunarYearDays[I] >= DayCount  then
      Break;

    DayCount := DayCount - LunarYearDays[I];
  end;

  // STEP 6
  Result.Month := 0;
  MonthTable := LunarMonthTable[I];
  for I := 1 to Length(MonthTable) do
  begin
    // 6-1
    if CharInSet(MonthTable[I], ['1', '2']) then
      Inc(Result.Month);

    // 6-2
    if CharInSet(MonthTable[I], ['1', '3']) then      // 소월은 29일
      MonDays := 29
    else if CharInSet(MonthTable[I], ['2', '4']) then // 대월은 30일
      MonDays := 30
    else
      raise Exception.CreateFmt('Incorrect lunar month table data(Index: %d, Char: %s)', [I, MonthTable[I]]);

    // 6-3
    if MonDays >= DayCount then
    begin
      if CharInSet(MonthTable[I], ['3', '4']) then
        Result.IsLeapMonth := True;
      Break;
    end;

    DayCount := DayCount - MonDays;
  end;

  // STEP 7
  Result.Day := DayCount;
end;

function SolarToLunar(ADate: TDate): TLunarDate; overload;
var
  mYear, mMonth, mDay:word;
begin
  DecodeDate(ADate, mYear, mMonth, mDay);
  result := SolarToLunar(mYear, mMonth, mDay);
end;

function SupportLunarStart: TLunarDate;
begin
  Result := toLunarDate(SupportYearStart, 1, 1, False);
end;

function SupportLunarEnd: TLunarDate;
begin
  Result := toLunarDate(SupportYearEnd, 12, GetLunarDaysOfMonth(SupportYearEnd, 12, False), False);
end;

function SupportSolarStart: TSolarDate;
begin
  Result := toSolarDate(SupportSolarDateStartYear, SupportSolarDateStartMonth, SupportSolarDateStartDay);
end;

function SupportSolarEnd: TSolarDate;
begin
  Result := toSolarDate(SupportSolarDateEndYear, SupportSolarDateEndMonth, SupportSolarDateEndDay);
end;


// 음력 달의 마지막 날을 반환한다.
function GetLunarDaysOfMonth(AYear, AMonth: Word; AIsLeapMonth: Boolean): Word;
var
  MonthTable: string;
  MonthIndex: Integer;
begin
  Result := 0;
  MonthTable := LunarMonthTable[AYear - SupportYearStart];

  MonthIndex := GetMonthToMonthIndex(AMonth, AIsLeapMonth, MonthTable);

  // 윤달요청 경우 예외
  if AIsLeapMonth and (not CharInSet(MonthTable[MonthIndex], ['3', '4'])) then
    Exit;

  Result := IfThen(CharInSet(MonthTable[MonthIndex], ['1', '3']), 29, 30);
end;

// 음력 달의 Index로 달번호와 윤달 여부를 반환한다.
function GetLunarMonthFromMonthIndex(AYear: Word;
  AIndexOfMonth: Integer; var AMonth: Word;
  var AIsLeapMonth: Boolean): Boolean;
var
  I: Integer;
  MonthTable: string;
begin
  Result := False;

  MonthTable := LunarMonthTable[AYear - SupportYearStart];

  // Index 에러
  if Length(MonthTable) < AIndexOfMonth then
    Exit;

  AMonth := AIndexOfMonth;
  AIsLeapMonth := CharInSet(MonthTable[AIndexOfMonth], ['3', '4']);
  for I := 1 to AIndexOfMonth do
  begin
    // Index 이전에 윤달이 있으면 윤달 제거
    if CharInSet(MonthTable[I], ['3', '4']) then
    begin
      AMonth := AIndexOfMonth - 1;
      Break;  // 윤달은 한번만 있음
    end;
  end;

  Result := True;
end;

end.
